package redisex

import (
	"context"
	"time"

	"github.com/go-redis/redis/v7"
	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
)

// RedisEX go-redis 封装
type RedisEX struct {
	client *redis.Client
}

// NewRedisEX new RedisEx client
func NewRedisEX(address, password string, db, maxRetries int, idleTimeout time.Duration) (*RedisEX, error) {
	redisClient := redis.NewClient(&redis.Options{
		Addr:        address,
		Password:    password, // no password set
		DB:          db,       // use default DB
		MaxRetries:  maxRetries,
		IdleTimeout: idleTimeout,
	})
	redisEx := &RedisEX{client: redisClient}
	err := redisEx.Ping()
	if err != nil {
		return nil, err
	}
	return redisEx, nil
}

func getSpan(span opentracing.Span) opentracing.Span {
	newSpan := span.Tracer().StartSpan("redis", opentracing.ChildOf(span.Context()))
	ext.SpanKindRPCClient.Set(newSpan)
	ext.DBType.Set(newSpan, "redis")
	return newSpan
}

// close
func (r *RedisEX) Close() error {
	return r.client.Close()
}

// Ping ping test
func (r *RedisEX) Ping() error {
	if _, err := r.client.Ping().Result(); err != nil {
		return err
	}
	return nil
}

// Do redis do
func (r *RedisEX) Do(ctx context.Context, args ...interface{}) (interface{}, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "do")
		span.LogKV("key", args)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.DoContext(ctx, args...).Result()
}

// Keys get keys
func (r *RedisEX) Keys(ctx context.Context, pattern string) ([]string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "do")
		span.LogKV("pattern", pattern)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Keys(pattern).Result()
}

// TTL key ttl
func (r *RedisEX) TTL(ctx context.Context, key string) (time.Duration, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "ttl")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).TTL(key).Result()
}

// Get redis get
func (r *RedisEX) Get(ctx context.Context, key string) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "get")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Get(key).Result()
}

// Set redis set
func (r *RedisEX) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "set")
		span.LogKV("key", key, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Set(key, value, expiration).Result()
}

// SetNX redis set
func (r *RedisEX) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "setnx")
		span.LogKV("key", key, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).SetNX(key, value, expiration).Result()
}

// Del redis delete
func (r *RedisEX) Del(ctx context.Context, keys ...string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "del")
		span.LogKV("key", keys)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Del(keys...).Result()
}

// Exists redis exists
func (r *RedisEX) Exists(ctx context.Context, keys ...string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "exists")
		span.LogKV("keys", keys)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Exists(keys...).Result()
}

// Expire expire
func (r *RedisEX) Expire(ctx context.Context, key string, expiration time.Duration) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "expire")
		span.LogKV("key", key, "expiration", expiration)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Expire(key, expiration).Result()
}

// ExpireAt expire time
func (r *RedisEX) ExpireAt(ctx context.Context, key string, tm time.Time) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "expire")
		span.LogKV("key", key, "expireAt", tm)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).ExpireAt(key, tm).Result()
}

// Decr decrease
func (r *RedisEX) Decr(ctx context.Context, key string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "decr")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Decr(key).Result()
}

// DecrBy decrease by value
func (r *RedisEX) DecrBy(ctx context.Context, key string, decrement int64) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "decr")
		span.LogKV("key", key, "decrement", decrement)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).DecrBy(key, decrement).Result()
}

// Incr increase
func (r *RedisEX) Incr(ctx context.Context, key string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "Incr")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Incr(key).Result()
}

// IncrBy increase by value
func (r *RedisEX) IncrBy(ctx context.Context, key string, increment int64) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "incrby")
		span.LogKV("key", key, "increment", increment)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).IncrBy(key, increment).Result()
}

// hash set

// HExists fields check
func (r *RedisEX) HExists(ctx context.Context, key, field string) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hexists")
		span.LogKV("key", key, "field", field)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HExists(key, field).Result()
}

// HLen fields number of a map
func (r *RedisEX) HLen(ctx context.Context, key, field string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hlen")
		span.LogKV("key", key, "field", field)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HLen(key).Result()
}

// HKeys fields of a map
func (r *RedisEX) HKeys(ctx context.Context, key string) ([]string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hkeys")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HKeys(key).Result()
}

// HGet get a field of a map
func (r *RedisEX) HGet(ctx context.Context, key, field string) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hget")
		span.LogKV("key", key, "field", field)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HGet(key, field).Result()
}

// HGetAll get all fields of a map
func (r *RedisEX) HGetAll(ctx context.Context, key string) (map[string]string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hgetall")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HGetAll(key).Result()
}

// HMGet get specified fields of a map
func (r *RedisEX) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hmget")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HMGet(key, fields...).Result()
}

// HSet set a field of a map
func (r *RedisEX) HSet(ctx context.Context, key, field string, value interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hset")
		span.LogKV("key", key, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HSet(key, field, value).Result()
}

// HMSet set a map as map type
func (r *RedisEX) HMSet(ctx context.Context, key string, values ...interface{}) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hmset")
		span.LogKV("key", key, "values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HMSet(key, values...).Result()
}

// HSetNX set a map as map type
func (r *RedisEX) HSetNX(ctx context.Context, key, field string, value interface{}) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hsetnx")
		span.LogKV("key", key, "field", field, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HSetNX(key, field, value).Result()
}

// HDel del val by field of a map
func (r *RedisEX) HDel(ctx context.Context, key string, fields ...string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "hsetnx")
		span.LogKV("key", key, "fields", fields)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).HDel(key, fields...).Result()
}

// LLen get list length
func (r *RedisEX) LLen(ctx context.Context, key string) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "llen")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LLen(key).Result()
}

// LIndex get item by index from a list
func (r *RedisEX) LIndex(ctx context.Context, key string, index int64) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lindex")
		span.LogKV("key", key, "index", index)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LIndex(key, index).Result()
}

// LInsert insert before or after a value
func (r *RedisEX) LInsert(ctx context.Context, key, op string, pivot, value interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "linsert")
		span.LogKV("key", key, "op", op, "pivot", pivot, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LInsert(key, op, pivot, value).Result()
}

// LInsertBefore insert before a specified value
func (r *RedisEX) LInsertBefore(ctx context.Context, key string, pivot, value interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "linsert before")
		span.LogKV("key", key, "pivot", pivot, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LInsertBefore(key, pivot, value).Result()
}

// LInsertAfter insert before or after a value
func (r *RedisEX) LInsertAfter(ctx context.Context, key string, pivot, value interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "linsert after")
		span.LogKV("key", key, "pivot", pivot, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LInsertAfter(key, pivot, value).Result()
}

// LPop pop a value from list
func (r *RedisEX) LPop(ctx context.Context, key string) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lpop")
		span.LogKV("key", key)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LPop(key).Result()
}

// LPush push a value to a list
func (r *RedisEX) LPush(ctx context.Context, key string, values ...interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lpush")
		span.LogKV("key", key, "values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LPush(key, values...).Result()
}

// LPushX push a value to list if that is exist
func (r *RedisEX) LPushX(ctx context.Context, key string, values ...interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lpushx")
		span.LogKV("key", key, "values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LPushX(key, values...).Result()
}

// RPush push a value to a list
func (r *RedisEX) RPush(ctx context.Context, key string, values ...interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "rpush")
		span.LogKV("key", key, "values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).RPush(key, values...).Result()
}

// RPushX push a value to list if that is exist
func (r *RedisEX) RPushX(ctx context.Context, key string, values ...interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "rpushx")
		span.LogKV("key", key, "values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).RPushX(key, values...).Result()
}

// LRange get items from a list
func (r *RedisEX) LRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lrange")
		span.LogKV("key", key, "start", start, "stop", stop)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LRange(key, start, stop).Result()
}

// LRem remove values in a list
func (r *RedisEX) LRem(ctx context.Context, key string, count int64, value interface{}) (int64, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lrem")
		span.LogKV("key", key, "count", count, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LRem(key, count, value).Result()
}

// LSet add a value to list with position
func (r *RedisEX) LSet(ctx context.Context, key string, index int64, value interface{}) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lset")
		span.LogKV("key", key, "index", index, "value", value)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LSet(key, index, value).Result()
}

// LTrim remove values in a list with position
func (r *RedisEX) LTrim(ctx context.Context, key string, start, stop int64) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "lset")
		span.LogKV("key", key, "start", start, "stop", stop)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).LTrim(key, start, stop).Result()
}

// MSet add multiple values with keys
func (r *RedisEX) MSet(ctx context.Context, values ...interface{}) (string, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "mset")
		span.LogKV("values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).MSet(values).Result()
}

// MSetNX add multiple values with keys if not exist
func (r *RedisEX) MSetNX(ctx context.Context, values ...interface{}) (bool, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "msetnx")
		span.LogKV("values", values)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).MSetNX(values).Result()
}

// MGet get multiple values with keys
func (r *RedisEX) MGet(ctx context.Context, keys ...string) ([]interface{}, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "msetnx")
		span.LogKV("keys", keys)
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).MGet(keys...).Result()
}

// TxPipelined acts like Pipeline, but wraps queued commands with MULTI/EXEC.
func (r *RedisEX) TxPipelined(ctx context.Context, fn func(pipe redis.Pipeliner) error) ([]redis.Cmder, error) {
	// tracing settings
	if span := opentracing.SpanFromContext(ctx); span != nil {
		span := getSpan(span)
		ext.DBStatement.Set(span, "tx pipelined")
		defer span.Finish()
		ctx = opentracing.ContextWithSpan(ctx, span)
	}
	return r.client.WithContext(ctx).Pipelined(fn)
}
